#!/usr/bin/python
# Copyright 2007 Alex K (wtwf.com) All rights reserved.
# $Id: bookmarks.py,v 1.15 2008-04-03 15:48:13 ark Exp $

"""Load some bookmarks from google bookmarks and create some files from them.
"""

try:
  from xml.etree import ElementTree # for Python 2.5 users
except:
  from elementtree import ElementTree

import ConfigParser
import datetime
import getopt
import getpass
import httplib
import logging
import os
import stat
import sys
import time
import urllib
import urllib2

def usage(code, msg=''):
  if code:
    fd = sys.stderr
  else:
    fd = sys.stdout
  PROGRAM = os.path.basename(sys.argv[0])
  print >> fd, __doc__ % locals()
  if msg:
    print >> fd, msg
  sys.exit(code)


class Bookmarks:

  def __init__(self, email, password,
               global_query_constraint=None,
               output_directory=""):
    """Authenticate with the Google server to get an SID cookie value.
    Set up some default values.
    """
    self.global_query_constraint = global_query_constraint
    self.output_directory = output_directory

    request_body = urllib.urlencode({'Email': email,
                                     'Passwd': password,
                                     'accountType': 'GOOGLE',
                                     'service': 'xapi',
                                     'source': 'arkbookmarks'})

    # Open a connection to the authentication server.
    auth_connection = httplib.HTTPSConnection('www.google.com')

    # Begin the POST request to the client login service.
    auth_connection.putrequest('POST', '/accounts/ClientLogin')
    # Set the required headers for an Account Authentication request.
    auth_connection.putheader('Content-type',
                              'application/x-www-form-urlencoded')
    auth_connection.putheader('Content-Length', str(len(request_body)))
    auth_connection.endheaders()

    logging.info("Authenticating")
    auth_connection.send(request_body)

    password = ""
    request_body = ""

    # Process the response and throw exceptions if the login request did not
    # succeed.
    auth_response = auth_connection.getresponse()
    if auth_response.status == 200:
      response_body = auth_response.read()
      cookies = response_body.splitlines()
    else:
      logging.fatal("Authentication Failed")
      sys.exit()
    # we want the SID cookie which is the first one
    # (this is prone to failure at some point)
    self.auth_cookie = cookies[0]

  def HandleSection(self, config, section):
    """Given a section work out what we really want and pass that onto
    CreateSection.
    """
    num = GetOption(config, section, "items", 10)
    output = GetOption(config, section, "output", "rss")

    filename = config.get(section, "filename")
    query = config.get(section, "query")

    if self.global_query_constraint:
      query = " ".join((query, self.global_query_constraint))

    self.CreateSection(query, num, output, filename)

  def CreateSection(self, query, num, output, filename):
    """Create a section from a query with a number of items in a
    particular output. Data is written to filename.

    NOTE: if output is 'list' the rss will also be written as filename
    without an extension and .xml added.
    """
    # Keep a copy of what they wanted.
    req_output = output

    if output == 'list':
      req_output = 'rss'  # We'll post process it to get list output.

    request_body = urllib.urlencode({'q': query,
                                     'output': req_output,
                                     'num': num,
                                     'sort': ''})

    req = urllib2.Request(url='http://www.google.com/bookmarks/find?' +
                          request_body)

    req.add_header('Cookie', self.auth_cookie)
    logging.info("Requesting data for: %s", query)
    f = urllib2.urlopen(req)
    txt = f.read()

    if output == 'list':
      # O.K. write out the rss feed and then convert to html and write that out
      rss_filename = "%s.xml" % os.path.splitext(filename)[0]

      out = open(os.path.join(self.output_directory, rss_filename), "w")
      out.write(txt)
      out.close()

      # Post process a little.....
      # Replace elements with links that use <ul><li>
      logging.info("converting rss to list format")
      tree = ElementTree.fromstring(txt)
      channel = tree.find("channel")
      bookmarks = channel.findall("item")
      txt = [u"<ul>"]
      for bookmark in bookmarks:
        title = Abbreviate(bookmark.findtext("title"))
        url = bookmark.findtext("link")
        txt.append(u'  <li><a href="%s">%s</a></li>' % (url, title))
        if len(txt) > num:
          break
      txt.append(u"</ul>")
      txt = u"\n".join(txt)

    out = open(os.path.join(self.output_directory, filename), "w")
    out.write(txt.encode('utf-8'))
    out.close()


  def HandleUrl(self, config, section):
    """Given a section about a url, load the url and write it to a file.
    """
    filename = config.get(section, "filename")
    url = config.get(section, "url")

    req = urllib2.Request(url=url)
    req.add_header('Cookie', self.auth_cookie)
    req.add_header('User-Agent', 'blocking-specific-user-agents-is-retarded')
    logging.info("Requesting data for: %s", url)
    f = urllib2.urlopen(req)
    out = open(os.path.join(self.output_directory, filename), "w")
    out.write(f.read())
    out.close()
    f.close()


def GetOption(config, section, name, default):
  """Get an option from the config, first look in the section provided
  and then look in the global section, if none found return the
  default.

  Args:
    config a ConfigParser.parse'd object
    section what section shall we first look in (string)
    name what is the name we are looking for. (string)
    default returned if nothing found in section or 'global' (string)
  Return:
    A value from the config or the default value (string)
  """
  if config.has_option(section, name):
    return config.get(section, name)
  else:
    if config.has_option("global", name):
      return config.get("global", name)
  return default

def Abbreviate(str, max_length=60):
  """Truncate a string that's too long. End of string is replaced with "..."
  """
  if len(str) > max_length:
    return str[0:max_length - 5] + "..."
  return str

def RunMonitor(monitor_file, run_every):
  """Monitor to make sure that this script is running regularly. If it
  fails an error message is printed out. If everything is O.K. nothing
  is printed out, making this perfect for a crontab entry.
  """
  if run_every == 0:
    logging.fatal("Cannot run monitor since run_every is zero")
    sys.exit(1)
  if not os.path.exists(monitor_file):
    logging.fatal("monitor_file '%s' does not exist", monitor_file)
    sys.exit(2)

  mod_time = os.stat(monitor_file)[stat.ST_MTIME]
  how_old = int(time.time()) - mod_time
  max_age = 2 * run_every * 60
  if how_old > max_age:
    logging.fatal("monitor_file '%s' is too old\n"
                  "%s > %s\n"
                  "You should restart bookmarks.py", monitor_file,
                  str(datetime.timedelta(seconds=how_old)),
                  str(datetime.timedelta(seconds=max_age)))
    sys.exit(3)
  # logging.info("Monitor O.K. %s < %s",
  #              str(datetime.timedelta(seconds=how_old)),
  #              str(datetime.timedelta(seconds=max_age)))

def main():
  """The main function runs the Bookmarks application with the provided
  username and password values.  Authentication credentials are required.
  """

  logging.basicConfig()
  logging.getLogger().setLevel(logging.DEBUG)

  # parse command line options
  try:
    opts, args = getopt.getopt(sys.argv[1:], "cm", ["email=", "password=",
                                                    "config=", "monitor"])
  except getopt.error, msg:
    usage()

  # Process options
  config_filename = None
  monitor = False
  for opt, arg in opts:
    if opt in ("-m", "--monitor"):
      monitor = True
    if opt in ("-c", "--config"):
      config_filename = arg

  if config_filename is None:
    config_filename = os.path.expanduser("~/.bookmarksrc")
    if not os.path.exists(config_filename):
      usage("You must provide a config path, ~/.bookmarksrc doesn't exist")

  config = ConfigParser.ConfigParser()
  config.read([config_filename])

  email = GetOption(config, "global", "email", "")
  password = GetOption(config, "global", "password", "")

  global_query_constraint = GetOption(config, "global",
                                      "global_query_constraint", "")
  run_every = GetOption(config, "global", "run_every", 0)
  if not run_every or run_every <= 0:
    run_every = 0
  else:
    run_every = int(run_every)
  output_directory = GetOption(config, "global", "output_directory", "")
  if output_directory:
    output_directory = os.path.expanduser(output_directory)
  monitor_file = os.path.join(output_directory, ".bookmarksran")

  if monitor:
    RunMonitor(monitor_file, run_every)
    return

  logging.debug("Starting %s", " ".join(sys.argv))

  if not password:
    password = getpass.getpass("Enter password for '%s': " % email)

  while True:
    try:
      bookmarks = Bookmarks(email, password,
                            global_query_constraint,
                            output_directory)

      # for every section in config that starts with 'section'
      # do some processing.
      for section in config.sections():
        if section == "global":
          continue
        if section.startswith("group "):
          bookmarks.HandleSection(config, section)
        elif section.startswith("url "):
          bookmarks.HandleUrl(config, section)
        else:
          logging.fatal("Don't know how to handle section: %s", section)


      # now touch a file to prove we've run
      out = open(monitor_file, "w")
      out.write(time.asctime(time.gmtime()))
      out.close()
    except:
      logging.info("It failed :(")
      # raise
    if not run_every or run_every <= 0:
      break
    else:
      logging.info("Sleeping for %s minutes", run_every)
      time.sleep(int(run_every) * 60)

if __name__ == '__main__':
  main()
